<?php
//$HeadURL: http://instaforms.googlecode.com/svn/trunk/lib/implementation/InstaForm.php $
//$Revision: 21 $
//$Date: 2012-08-31 08:07:56 +0000 (Fri, 31 Aug 2012) $
//$Author: dev@arbrebinaire.com $
/**
 * Class extending Zend_Form
 *
 *
 * PHP version 5
 *
 * @package    com.arbrebinaire.instaforms
 * @author     Francois Hetu <dev@arbrebinaire.com>
 * @copyright  2012 Francois Hetu
 * @version    SVN: $Id: InstaForm.php 21 2012-08-31 08:07:56Z dev@arbrebinaire.com $
 */

require_once 'lib/bootstrap.php';

Zend_Loader::loadClass('Zend_Form');
Zend_Loader::loadClass('Zend_View');

class InstaForm extends Zend_Form{

    //The char that indicates which of the answer(s)
    //in a multiple choice form element is (are) preselected
    //Has to be regex escaped if need be
    const DEFAULT_ANSWERS_INDICATOR_CHAR = '\*';

    protected $formProperties;
    protected $elementList;
    protected $view;
    protected $decorators;
    protected $instaFormOptions;
    protected $csrfSalt = 'la poudre de perlimpinpin coûte cher';
    protected $csrfExpirationSeconds = 600;

    /**
     *
     * The class constructor
     *
     * @param array $formDescription The form description array
     * Format:
     * array(
     * 	'properties' => array(
     * 		array('name' => 'property_one', 'value' => 'value of property_one'),
     * 		array('name' => 'property_two', 'value' => 'value of property_two')
     * 	),
     *  'fields_data' => array(
     *  	//All the form element description arrays as described here: @see createElementByType
     *  )
     * )
     *
     * @param array $instaFormOptions An optional array of options specific to this class
     * @param Zend_View $view The view associated with this form
     * @param array $decorators An optional array of Zend_Form_Decorator(s)
     * @param array $zendFormOptions An optional array of Zend_Form options
     *
     * @return void
     */
    public function __construct($formDescription = null, array $instaFormOptions = null, Zend_View $view = null, array $decorators = null, array $zendFormOptions = null){

        if(!$formDescription){
            throw new Exception('InstaForm constructor needs a full form description');
        }

        $formProperties = $formDescription['properties'];

        if(!$formProperties){
            throw new Exception('InstaForm constructor needs an array of form properties descriptors');
        }

        $this->formProperties = $formProperties;

        $elementList = $formDescription['fields_data'];

        if(!$elementList){
            throw new Exception('InstaForm constructor needs an array of field element descriptors');
        }

        $this->elementList = $elementList;

        $instaFormsRequiredOptions = array(
        	'submit_value'                 => 'Submit',
            'empty_select_option_text'     => 'Please select an option',
        	'form_method'                  => 'post',
            'form_name'                    => 'instaform-form',
        	'form_id'                      => 'instaform-form',
            'form_action'                  => 'form.php',
            'form_description'             => 'Please fill out the following form.',
            'file_upload_path'             => '/var/upload',
            'file_upload_max_size'         => 102400, //100 kb
            'file_upload_valid_extensions' => 'jpg,gif,png,pdf'
        );

        $instaFormOptions = is_array($instaFormOptions) ? $instaFormOptions : array();

        $this->instaFormOptions = array_merge($instaFormsRequiredOptions, $instaFormOptions);

        if(!$view){
            $view = new Zend_View();
        }

        $this->view = $view;

        $this->decorators = $decorators;

        parent::__construct($zendFormOptions);
    }

    /**
     * Required by the Zend Form interface
     * @overides
     */
    public function init(){

        // Set properties of the form
        $this->setName(   $this->instaFormOptions['form_name'])
        ->setAction(      $this->instaFormOptions['form_action'])
        ->setMethod(      $this->instaFormOptions['form_method'])
        ->setAttrib('id', $this->instaFormOptions['form_id'])
        ->setDescription( $this->instaFormOptions['form_description']);

        foreach($this->elementList as $elementDescription){
            //Initialization of the options array
            $optionsArr = array(
                'label' => '',
                'filters' => array('StringTrim', 'StripTags')
            );

            if($this->decorators){
                $optionsArr['decorators'] = $this->decorators;
            }

            $element = $this->createElementByType($elementDescription);

            $optionsArr['label'] = $elementDescription['values']['sentence'];

            $optionsArr = $this->addValidationOptions($elementDescription, $optionsArr);

            $element->setOptions($optionsArr);

            $this->addElement($element);
        }

        //Preparing the anti-csrf hash
        $csrf = $this->createElement('hash', 'csrf', array('salt' => $this->csrfSalt, 'timeout' => $this->csrfExpirationSeconds));
        $csrf->setIgnore(true);
        $csrf->removeDecorator('label');

        $this->addElement($csrf);

        // Submit Button
        $submit = $this->createElement('submit', $this->instaFormOptions['submit_value']);

        // Add controls to the form
        $this->addElement($submit);

    }


    /**
     * Wrapper of the Zend render method, using the view stored as a property
     *
     * @return string the form HTML
     */
    public function render(){
        return parent::render($this->view);
    }


    /**
     * Retrieves the message shown after the form is submitted
     *
     * @return string The message shown after the form is submitted (new lines of textarea content converted to <br />s)
     */
    public function getPostSubmitMessage(){
        return nl2br($this->getFormPropertyValue('instaforms-form-postsubmitmessage'));
    }

    /**
     * Retrieves the message shown if the form is offline
     *
     * @return string The message shown if the form is offline (new lines of textarea content converted to <br />s)
     */
    public function getOfflineMessage(){
        return nl2br($this->getFormPropertyValue('instaforms-form-offlinemessage'));
    }

    /**
     * Retrieves the max number of answers allowed for this form
     *
     * @return number The max number of answers allowed for this form
     */
    public function getMaxNumberOfAnswers(){
        $maxNumber = $this->getFormPropertyValue('instaforms-form-maxanswers');
        return !$maxNumber ? INF : $maxNumber;
    }

    /**
     * Retrieves from the form elements array the element corresponding to the id parameter
     *
     * @param string $elementId
     * @return mixed The element description array, or null if not found
     */
    public function getElementById($elementId){
        $foundElement = null;
        foreach($this->elementList as $element){
            if($element['values']['id'] == $elementId){
                $foundElement = $element;
            }
        }
        return $foundElement;
    }

    /**
     *
     * Retrieves the "sentence" property: either the question or the section title
     *
     * @param string $elementId The element unique id
     *
     * @return string The element "sentence" property
     */
    public function getElementSentence($elementId){
        $element = $this->getElementById($elementId);
        return $element['values']['sentence'];
    }


    /**
     * Verifies if the form can be posted in regard of online status and date constraints
     *
     * @return Boolean True if both online and date properties constraints are true
     */
    public function canBeShown(){
        return $this->isOnline() && $this->isBetweenValidityDates();
    }


    /**
     *
     * Verifies if the form has an access control list
     *
     * @return boolean True if the form has some sort of ACL
     */
    public function isACLOn(){
        return $this->getFormPropertyValue('instaforms-form-status') == 1;
    }


    /**
     *
     * Verifies if the form can be posted online
     *
     * @return boolean True if the form can be posted online
     */
    public function isOnline(){
        return $this->getFormPropertyValue('instaforms-form-status') == 1;
    }


    /**
     *
     * Verifies if today is within the time constraint of the form
     *
     * @return boolean True if today is within the time constraint of the form
     */
    public function isBetweenValidityDates(){
        $startDateStr = $this->getFormPropertyValue('instaforms-form-startdate');
        $endDateStr = $this->getFormPropertyValue('instaforms-form-enddate');

        $startDate = strtotime($startDateStr);
        $endDate = strtotime($endDateStr);

        $now = strtotime('today');

        //Multiple cases:

        //No start date and no end date
        if(!$startDateStr && !$endDateStr) return true;

        //No start date: we check for end date only (Condition A)
        if(!$startDateStr && $endDateStr && $endDate >= $now) return true;

        //A start date, but no end date: we check for start date only (Condition B)
        if($startDateStr && !$endDateStr && $startDate <= $now) return true;

        if($startDateStr && $endDateStr){
            //We both have a start and end date
            if($endDate < $startDate){
                //End date is wrongly before start date: we only care about end date
                return $endDate >= $now;
            }else{
                //We have a valid start and end date
                return $startDate <= $now && $endDate >= $now;
            }
        }
         
        //Only one of the dates is empty, and neither condition A nor B is met: has to be false
         
        return false;
    }

    /**
     * Does this form have file upload elements
     * Useful in the form controller to check if upload was successful
     * 
     * @return boolean True if form has at least one file upload element
     */
    public function hasFileUpload(){
        return count($this->getFormElementsByType('file')) > 0;
    }
    
    public function receiveAllFiles(){
        $fileUploadElements = $this->getFormElementsByType('file');
        $success = true;
        foreach($fileUploadElements as $elementArr){
        	$element = $this->getElement($elementArr['values']['id']);
        	$intermediateSuccess = $element->receive();
        	if($intermediateSuccess){
        	    $location = $element->getFileName();
        	    //rename()
        	}
        	$success = $success && $intermediateSuccess;
        }
        return $success;
    }
    
    /**
     * 
     * Retrieves all form elements of a certain type
     * 
     * @param string $type The element type. Please see @see createElementByType switch statement for available element types
     * @return array The elements of type $type
     */
    private function getFormElementsByType($type){
        $foundElements = array();
        foreach($this->elementList as $element){
            if($element['type'] == $type){
                $foundElements[] = $element;
            }
        }
        return $foundElements;
    }
    
    /**
     *
     * Extracts the property value from the form definition array
     *
     * @param string $propertyName
     * @throws Exception If a property of the given name does not exist
     * @return string The value of the named form property
     */
    private function getFormPropertyValue($propertyName){
        foreach ($this->formProperties as $property) {
            if($property['name'] == $propertyName){
                return $property['value'];
            }
        }
        throw new Exception('Form property "' . $propertyName . '" is unknown');
    }

    /**
     *
     * This method creates the Zend form element in accordance with the element description array parameter
     *
     * @param array $elementDescription A form element description array of the following structure:
     *
     * array(
     * 	'type'   =>'shorttext',
     *  'values' => array(
     *    'id' => 'instaform_some_unique_sha1_hash'
     *    'sentence' => 'The question or section title'
     *    'validation' => array(
     *    	'required' => 'true|false'  // This is a string, and not a boolean because of over the wire json transmission constraints
     *      'email'    => 'true|false'
     *    ),
     *    'answers' => array(
     *     '*Answer one',  //The prefixed char * is to denote a default answer
     *     'Answer two',
     *    )
     *  )
     * )
     *
     * @return Zend_Form_Element The element created according to its definition
     *
     */
    private function createElementByType(array $elementDescription){

        $element = null;

        //The form element unique id
        //This id is unique across time
        $id = $elementDescription['values']['id'];

        //The element type as defined by InstaForms
        //Please see switch below for a description of the InstaForms element types
        $type = $elementDescription['type'];

        //For multiple-answer element types, the answers array
        $answers = $elementDescription['values']['answers'];

        //This pattern for default answers discrimination
        //Can be preceded by blanks or not
        $selectedPattern = '/^\s*' . self::DEFAULT_ANSWERS_INDICATOR_CHAR . '+/i';

        switch($type){
            //An input form element
            case 'shorttext':
                $element = $this->createElement('text', $id);
                break;
                //A textarea form element
            case 'longtext':
                $element = $this->createElement('textarea', $id);
                break;
                //A password form element
            case 'password':
                $element = $this->createElement('password', $id);
                break;
                //A select form element
            case 'multiple_exclusive':
                $element = $this->createElement('select', $id);
                $selected = '';
                $element->addMultiOption($selected, $this->instaFormOptions['empty_select_option_text']);
                foreach($answers as $answer){
                    $replaced = preg_replace($selectedPattern, '', $answer);
                    //The selected option will differ from the answer if the prefix is found.
                    //It should also be empty
                    //(otherwise the user has wrongly chosen two options and a previous option was already selected.
                    //We want the first one only)
                    if($answer != $replaced && $selected == ''){
                        $selected = $replaced;
                    }

                    $element->addMultiOption($replaced, $replaced);
                }
                if($selected){
                    $element->setValue($selected);
                }
                break;
                //A checkbox group form element
            case 'multiple_inclusive':
                $element = $this->createElement('multiCheckbox', $id);
                $selectedCheckboxes = array();
                foreach($answers as $answer){
                    $replaced = preg_replace($selectedPattern, '', $answer);
                    if($answer !== $replaced){
                        $selectedCheckboxes[] = $replaced;
                    }
                    $element->addMultiOption($replaced, $replaced);
                }
                $element->setValue($selectedCheckboxes);
                break;
                //A file input form element
            case 'file':
                $element = $this->createElement('file', $id);
                //Adding specific file validators
                $element->addValidator('Count', false, 1);
                $element->addValidator('Size', false, $this->instaFormOptions['file_upload_max_size']);
                $element->addValidator('Extension', false, $this->instaFormOptions['file_upload_valid_extensions']);
                //Where to save the file
                $element->setDestination($this->instaFormOptions['file_upload_path']);
                //Setting the form proper enctype
                $this->setAttrib('enctype', 'multipart/form-data');
                break;
                //A form section title
                //We create a hidden input, which value is ignored (we only need the label)
            case 'section':
                $element = $this->createElement('hidden', $id);
                $element->setIgnore(true);
                break;
            default:
                throw new Exception('Unknown element type: '. $type);
        }
        return $element;
    }


    /**
     *
     * Modifies the Zend form element option array by adding the requested validators
     *
     * @param array $elementDescription The element description array as described here: @see createElementByType
     * @param array $optionsArr An option array as needed by a Zend form element
     *
     * @return array The Zend form element option array with the added "validators" sub array and the "required" property set to true or false
     */
    private function addValidationOptions($elementDescription, $optionsArr){

        $validationItems = $elementDescription['values']['validation'];

        $zendValidatorsArray = array();

        //Note that the validation value is passed through the wire as a string, which is either "true" or "false"
        $isRequired = false;

        foreach($validationItems as $key=>$validationValue){

            //#TODO Implement proper file validation
            if($elementDescription['type'] == 'file') continue;

            switch($key){
                case 'required':
                    $validationValue = $validationValue == 'true';
                    if($validationValue){
                        $isRequired = true;
                        $zendValidatorsArray[] = 'NotEmpty';
                    }
                    break;
                case 'email':
                    $validationValue = $validationValue == 'true';
                    if($validationValue){
                        $zendValidatorsArray[] = 'EmailAddress';
                    }
                    break;
                default:
                    throw new Exception('Unimplemented validation type: '. $key);
                    break;
            }
        }
        $optionsArr['required'] = $isRequired;
        $optionsArr['validators'] = $zendValidatorsArray;
        return $optionsArr;
    }

}